/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007-2008 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.spring.transaction.manager;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.transaction.support.SimpleTransactionStatus;

import com.genia.toolbox.spring.transaction.bean.EnlistableResource;
import com.genia.toolbox.spring.transaction.bean.impl.EnlistableResourceAggregator;

/**
 * A {@link PlatformTransactionManager} that delegate to another existing
 * {@link PlatformTransactionManager} but allows to enlist transactional element
 * dynamically.
 */
public class EnlistablePlatformTransactionManager
    implements PlatformTransactionManager
{

  /**
   * the <code>Log</code> variabe.
   */
  private static final Logger LOGGER = LoggerFactory.getLogger(EnlistablePlatformTransactionManager.class);

  /**
   * a {@link PlatformTransactionManager} to delegate to.
   */
  private PlatformTransactionManager mainPlatformTransactionManager;

  /**
   * the list of {@link EnlistableResource} to use.
   */
  private transient EnlistableResource resource;



  /**
   * Commit the given transaction, with regard to its status. If the transaction
   * has been marked rollback-only programmatically, perform a rollback.
   * <p>
   * If the transaction wasn't a new one, omit the commit for proper
   * participation in the surrounding transaction. If a previous transaction has
   * been suspended to be able to create a new one, resume the previous
   * transaction after committing the new one.
   * <p>
   * Note that when the commit call completes, no matter if normally or throwing
   * an exception, the transaction must be fully completed and cleaned up. No
   * rollback call should be expected in such a case.
   * <p>
   * If this method throws an exception other than a TransactionException, then
   * some before-commit error caused the commit attempt to fail. For example, an
   * O/R Mapping tool might have tried to flush changes to the database right
   * before commit, with the resulting DataAccessException causing the
   * transaction to fail. The original exception will be propagated to the
   * caller of this commit method in such a case.
   * 
   * @param status
   *          object returned by the <code>getTransaction</code> method
   * @throws TransactionException
   *           in case of lookup, creation, or system errors
   * @see TransactionStatus#setRollbackOnly
   * @see org.springframework.transaction.PlatformTransactionManager#commit(org.springframework.transaction.TransactionStatus)
   */
  public void commit(TransactionStatus status)
      throws TransactionException
  {
    synchronized (this) {
      if (status.isRollbackOnly()) {
        rollback(status);
        if (status.isNewTransaction()) {
          throw new UnexpectedRollbackException("Trying to commit a rollback only transaction.");
        }
        return;
      }
      try {
        resource.prepare(status);
      }
      catch (Exception e) {
        if (getMainPlatformTransactionManager() != null) {
          try {
            getMainPlatformTransactionManager().rollback(status);
          }
          catch (Exception rollbackException) {
            LOGGER.warn(rollbackException.getMessage(), rollbackException);
          }
        }
        throw new UnexpectedRollbackException("One of the enlisted resource failed to prepare for commiting a transaction", e);
      }
      if (getMainPlatformTransactionManager() != null) {
        try {
          getMainPlatformTransactionManager().commit(status);
        }
        catch (TransactionException e) {
          try {
            resource.rollback(status);
          }
          catch (Exception rollbackException) {
            LOGGER.warn(rollbackException.getMessage(), rollbackException);
          }
          throw e;
        }
      }
      try {
        resource.commit(status);
      }
      catch (Exception e) {
        new TransactionSystemException("An enlisted resource succeded in its prepare statement, but failed in its commit statement. The state of the application might be incoherent.", e);
      }
    }
  }



  /**
   * getter for the mainPlatformTransactionManager property.
   * 
   * @return the mainPlatformTransactionManager
   */
  public PlatformTransactionManager getMainPlatformTransactionManager()
  {
    return mainPlatformTransactionManager;
  }



  /**
   * Return a currently active transaction or create a new one, according to the
   * specified propagation behavior.
   * <p>
   * Note that parameters like isolation level or timeout will only be applied
   * to new transactions, and thus be ignored when participating in active ones.
   * <p>
   * Furthermore, not all transaction definition settings will be supported by
   * every transaction manager: A proper transaction manager implementation
   * should thrown an exception when unsupported settings are encountered.
   * <p>
   * An exception to the above rule is the read-only flag, which should be
   * ignored if no explicit read-only mode is supported. Essentially, the
   * read-only flag is just a hint for potential optimization.
   * 
   * @param definition
   *          TransactionDefinition instance (can be <code>null</code> for
   *          defaults), describing propagation behavior, isolation level,
   *          timeout etc.
   * @return transaction status object representing the new or current
   *         transaction
   * @throws TransactionException
   *           in case of lookup, creation, or system errors
   * @see TransactionDefinition#getPropagationBehavior
   * @see TransactionDefinition#getIsolationLevel
   * @see TransactionDefinition#getTimeout
   * @see TransactionDefinition#isReadOnly
   * @see org.springframework.transaction.PlatformTransactionManager#getTransaction(org.springframework.transaction.TransactionDefinition)
   */
  public TransactionStatus getTransaction(TransactionDefinition definition)
      throws TransactionException
  {
    synchronized (this) {
      TransactionStatus status = null;
      if (getMainPlatformTransactionManager() == null) {
        status = new SimpleTransactionStatus();
      }
      else {
        status = getMainPlatformTransactionManager().getTransaction(definition);
      }
      try {
        resource.startTransaction(definition, status);
      }
      catch (Exception e) {
        if (getMainPlatformTransactionManager() != null) {
          try {
            getMainPlatformTransactionManager().rollback(status);
          }
          catch (Exception rollbackException) {
            LOGGER.warn(rollbackException.getMessage(), rollbackException);
          }
        }
        throw new CannotCreateTransactionException("One of the enlisted resource failed to open a transaction", e);
      }
      return status;
    }
  }



  /**
   * Perform a rollback of the given transaction.
   * <p>
   * If the transaction wasn't a new one, just set it rollback-only for proper
   * participation in the surrounding transaction. If a previous transaction has
   * been suspended to be able to create a new one, resume the previous
   * transaction after rolling back the new one.
   * <p>
   * <b>Do not call rollback on a transaction if commit threw an exception.</b>
   * The transaction will already have been completed and cleaned up when commit
   * returns, even in case of a commit exception. Consequently, a rollback call
   * after commit failure will lead to an IllegalTransactionStateException.
   * 
   * @param status
   *          object returned by the <code>getTransaction</code> method
   * @throws TransactionException
   *           in case of lookup, creation, or system errors
   * @see org.springframework.transaction.PlatformTransactionManager#rollback(org.springframework.transaction.TransactionStatus)
   */
  public void rollback(TransactionStatus status)
      throws TransactionException
  {
    synchronized (this) {
      Exception error = null;
      if (getMainPlatformTransactionManager() != null) {
        try {
          getMainPlatformTransactionManager().rollback(status);
        }
        catch (Exception rollbackException) {
          error = rollbackException;
        }
      }
      try {
        resource.rollback(status);
      }
      catch (Exception rollbackException) {
        if (error == null) {
          error = rollbackException;
        }
        else {
          LOGGER.warn(rollbackException.getMessage(), rollbackException);
        }
      }
      if (error != null) {
        if (error instanceof TransactionException) {
          throw (TransactionException) error;
        }
        throw new TransactionSystemException("Exception happened during rollback", error);
      }
    }
  }



  /**
   * setter for the mainPlatformTransactionManager property.
   * 
   * @param mainPlatformTransactionManager
   *          the mainPlatformTransactionManager to set
   */
  public void setMainPlatformTransactionManager(PlatformTransactionManager mainPlatformTransactionManager)
  {
    this.mainPlatformTransactionManager = mainPlatformTransactionManager;
  }



  /**
   * setter for the resources property.
   * 
   * @param resources
   *          the resources to set
   */
  public void setResources(List<? extends EnlistableResource> resources)
  {
    EnlistableResourceAggregator enlistableResourceAggregator = new EnlistableResourceAggregator();
    enlistableResourceAggregator.setResources(resources);
    this.resource = enlistableResourceAggregator;
  }

}
